var i = 0;
var TEXT = i++;
var BEFORE_TAG_NAME = i++; //after <
var IN_TAG_NAME = i++;
var IN_SELF_CLOSING_TAG = i++;
var BEFORE_CLOSING_TAG_NAME = i++;
var IN_CLOSING_TAG_NAME = i++;
var AFTER_CLOSING_TAG_NAME = i++;
//attributes
var BEFORE_ATTRIBUTE_NAME = i++;
var IN_ATTRIBUTE_NAME = i++;
var AFTER_ATTRIBUTE_NAME = i++;
var BEFORE_ATTRIBUTE_VALUE = i++;
var IN_ATTRIBUTE_VALUE_DQ = i++; // "
var IN_ATTRIBUTE_VALUE_SQ = i++; // '
var IN_ATTRIBUTE_VALUE_NQ = i++;
//declarations
var BEFORE_DECLARATION = i++; // !
var IN_DECLARATION = i++;
//processing instructions
var IN_PROCESSING_INSTRUCTION = i++; // ?
//special tags
var BEFORE_SPECIAL = i++; //S
var BEFORE_SPECIAL_END = i++; //S
var BEFORE_SCRIPT_1 = i++; //C
var BEFORE_SCRIPT_2 = i++; //R
var BEFORE_SCRIPT_3 = i++; //I
var BEFORE_SCRIPT_4 = i++; //P
var BEFORE_SCRIPT_5 = i++; //T
var AFTER_SCRIPT_1 = i++; //C
var AFTER_SCRIPT_2 = i++; //R
var AFTER_SCRIPT_3 = i++; //I
var AFTER_SCRIPT_4 = i++; //P
var AFTER_SCRIPT_5 = i++; //T
var BEFORE_STYLE_1 = i++; //T
var BEFORE_STYLE_2 = i++; //Y
var BEFORE_STYLE_3 = i++; //L
var BEFORE_STYLE_4 = i++; //E
var AFTER_STYLE_1 = i++; //T
var AFTER_STYLE_2 = i++; //Y
var AFTER_STYLE_3 = i++; //L
var AFTER_STYLE_4 = i++; //E
var BEFORE_ENTITY = i++; //&
var BEFORE_NUMERIC_ENTITY = i++; //#
var IN_NAMED_ENTITY = i++;
var IN_NUMERIC_ENTITY = i++;
var IN_HEX_ENTITY = i++; //X

var j = 0;
var SPECIAL_NONE = j++;
var SPECIAL_SCRIPT = j++;
var SPECIAL_STYLE = j++;

export function Tokenizer(cbs) {
	this._state = TEXT;
	this._buffer = "";
	this._sectionStart = 0;
	this._index = 0;
	this._bufferOffset = 0;
	this._baseState = TEXT;
	this._special = SPECIAL_NONE;
	this._cbs = cbs;
	this._running = true;
	this._ended = false;
}

const whitespace = (c) => {
	return /\s/.test(c);
};

const ifElseState = (upper, SUCCESS, FAILURE) => {
	var lower = upper.toLowerCase();
	if (upper === lower) {
		return function (c) {
			if (c === lower) {
				this._state = SUCCESS;
			} else {
				this._state = FAILURE;
				this._index--;
			}
		};
	} else {
		return function (c) {
			if (c === lower || c === upper) {
				this._state = SUCCESS;
			} else {
				this._state = FAILURE;
				this._index--;
			}
		};
	}
};

const consumeSpecialNameChar = (upper, NEXT_STATE) => {
	var lower = upper.toLowerCase();
	return function (c) {
		if (c === lower || c === upper) {
			this._state = NEXT_STATE;
		} else {
			this._state = IN_TAG_NAME;
			this._index--;
		}
	};
};

Tokenizer.prototype._stateText = function (c) {
	if (c === "<") {
		if (this._index > this._sectionStart) {
			this._cbs.ontext(this._getSection());
		}
		this._state = BEFORE_TAG_NAME;
		this._sectionStart = this._index;
	}
};

Tokenizer.prototype._stateBeforeTagName = function (c) {
	if (c === "/") {
		this._state = BEFORE_CLOSING_TAG_NAME;
	} else if (c === "<") {
		this._cbs.ontext(this._getSection());
		this._sectionStart = this._index;
	} else if (c === ">" || this._special !== SPECIAL_NONE || whitespace(c)) {
		this._state = TEXT;
	} else if (c === "!") {
		this._state = BEFORE_DECLARATION;
		this._sectionStart = this._index + 1;
	} else if (c === "?") {
		this._state = IN_PROCESSING_INSTRUCTION;
		this._sectionStart = this._index + 1;
	} else {
		this._state = c === "s" || c === "S" ? BEFORE_SPECIAL : IN_TAG_NAME;
		this._sectionStart = this._index;
	}
};

Tokenizer.prototype._stateInTagName = function (c) {
	if (c === "/" || c === ">" || whitespace(c)) {
		this._emitToken("onopentagname");
		this._state = BEFORE_ATTRIBUTE_NAME;
		this._index--;
	}
};

Tokenizer.prototype._stateBeforeCloseingTagName = function (c) {
	if (whitespace(c));
	else if (c === ">") {
		this._state = TEXT;
	} else if (this._special !== SPECIAL_NONE) {
		if (c === "s" || c === "S") {
			this._state = BEFORE_SPECIAL_END;
		} else {
			this._state = TEXT;
			this._index--;
		}
	} else {
		this._state = IN_CLOSING_TAG_NAME;
		this._sectionStart = this._index;
	}
};

Tokenizer.prototype._stateInCloseingTagName = function (c) {
	if (c === ">" || whitespace(c)) {
		this._emitToken("onclosetag");
		this._state = AFTER_CLOSING_TAG_NAME;
		this._index--;
	}
};

Tokenizer.prototype._stateAfterCloseingTagName = function (c) {
	if (c === ">") {
		this._state = TEXT;
		this._sectionStart = this._index + 1;
	}
};

Tokenizer.prototype._stateBeforeAttributeName = function (c) {
	if (c === ">") {
		this._cbs.onopentagend();
		this._state = TEXT;
		this._sectionStart = this._index + 1;
	} else if (c === "/") {
		this._state = IN_SELF_CLOSING_TAG;
	} else if (!whitespace(c)) {
		this._state = IN_ATTRIBUTE_NAME;
		this._sectionStart = this._index;
	}
};

Tokenizer.prototype._stateInSelfClosingTag = function (c) {
	if (c === ">") {
		this._cbs.onselfclosingtag();
		this._state = TEXT;
		this._sectionStart = this._index + 1;
	} else if (!whitespace(c)) {
		this._state = BEFORE_ATTRIBUTE_NAME;
		this._index--;
	}
};

Tokenizer.prototype._stateInAttributeName = function (c) {
	if (c === "=" || c === "/" || c === ">" || whitespace(c)) {
		this._cbs.onattribname(this._getSection());
		this._sectionStart = -1;
		this._state = AFTER_ATTRIBUTE_NAME;
		this._index--;
	}
};

Tokenizer.prototype._stateAfterAttributeName = function (c) {
	if (c === "=") {
		this._state = BEFORE_ATTRIBUTE_VALUE;
	} else if (c === "/" || c === ">") {
		this._cbs.onattribend();
		this._state = BEFORE_ATTRIBUTE_NAME;
		this._index--;
	} else if (!whitespace(c)) {
		this._cbs.onattribend();
		this._state = IN_ATTRIBUTE_NAME;
		this._sectionStart = this._index;
	}
};

Tokenizer.prototype._stateBeforeAttributeValue = function (c) {
	if (c === '"') {
		this._state = IN_ATTRIBUTE_VALUE_DQ;
		this._sectionStart = this._index + 1;
	} else if (c === "'") {
		this._state = IN_ATTRIBUTE_VALUE_SQ;
		this._sectionStart = this._index + 1;
	} else if (!whitespace(c)) {
		this._state = IN_ATTRIBUTE_VALUE_NQ;
		this._sectionStart = this._index;
		this._index--; //reconsume token
	}
};

Tokenizer.prototype._stateInAttributeValueDoubleQuotes = function (c) {
	if (c === '"') {
		this._emitToken("onattribdata");
		this._cbs.onattribend();
		this._state = BEFORE_ATTRIBUTE_NAME;
	}
};

Tokenizer.prototype._stateInAttributeValueSingleQuotes = function (c) {
	if (c === "'") {
		this._emitToken("onattribdata");
		this._cbs.onattribend();
		this._state = BEFORE_ATTRIBUTE_NAME;
	}
};

Tokenizer.prototype._stateInAttributeValueNoQuotes = function (c) {
	if (whitespace(c) || c === ">") {
		this._emitToken("onattribdata");
		this._cbs.onattribend();
		this._state = BEFORE_ATTRIBUTE_NAME;
		this._index--;
	}
};

Tokenizer.prototype._stateBeforeDeclaration = function (c) {
	this._state = IN_DECLARATION;
};

Tokenizer.prototype._stateInDeclaration = function (c) {
	if (c === ">") {
		this._cbs.ondeclaration(this._getSection());
		this._state = TEXT;
		this._sectionStart = this._index + 1;
	}
};

Tokenizer.prototype._stateInProcessingInstruction = function (c) {
	if (c === ">") {
		this._cbs.onprocessinginstruction(this._getSection());
		this._state = TEXT;
		this._sectionStart = this._index + 1;
	}
};

Tokenizer.prototype._stateBeforeSpecial = function (c) {
	if (c === "c" || c === "C") {
		this._state = BEFORE_SCRIPT_1;
	} else if (c === "t" || c === "T") {
		this._state = BEFORE_STYLE_1;
	} else {
		this._state = IN_TAG_NAME;
		this._index--; //consume the token again
	}
};

Tokenizer.prototype._stateBeforeSpecialEnd = function (c) {
	if (this._special === SPECIAL_SCRIPT && (c === "c" || c === "C")) {
		this._state = AFTER_SCRIPT_1;
	} else if (this._special === SPECIAL_STYLE && (c === "t" || c === "T")) {
		this._state = AFTER_STYLE_1;
	} else this._state = TEXT;
};

Tokenizer.prototype._stateBeforeScript1 = consumeSpecialNameChar("R", BEFORE_SCRIPT_2);
Tokenizer.prototype._stateBeforeScript2 = consumeSpecialNameChar("I", BEFORE_SCRIPT_3);
Tokenizer.prototype._stateBeforeScript3 = consumeSpecialNameChar("P", BEFORE_SCRIPT_4);
Tokenizer.prototype._stateBeforeScript4 = consumeSpecialNameChar("T", BEFORE_SCRIPT_5);

Tokenizer.prototype._stateBeforeScript5 = function (c) {
	if (c === "/" || c === ">" || whitespace(c)) {
		this._special = SPECIAL_SCRIPT;
	}
	this._state = IN_TAG_NAME;
	this._index--;
};

Tokenizer.prototype._stateAfterScript1 = ifElseState("R", AFTER_SCRIPT_2, TEXT);
Tokenizer.prototype._stateAfterScript2 = ifElseState("I", AFTER_SCRIPT_3, TEXT);
Tokenizer.prototype._stateAfterScript3 = ifElseState("P", AFTER_SCRIPT_4, TEXT);
Tokenizer.prototype._stateAfterScript4 = ifElseState("T", AFTER_SCRIPT_5, TEXT);

Tokenizer.prototype._stateAfterScript5 = function (c) {
	if (c === ">" || whitespace(c)) {
		this._special = SPECIAL_NONE;
		this._state = IN_CLOSING_TAG_NAME;
		this._sectionStart = this._index - 6;
		this._index--;
	} else this._state = TEXT;
};

Tokenizer.prototype._stateBeforeStyle1 = consumeSpecialNameChar("Y", BEFORE_STYLE_2);
Tokenizer.prototype._stateBeforeStyle2 = consumeSpecialNameChar("L", BEFORE_STYLE_3);
Tokenizer.prototype._stateBeforeStyle3 = consumeSpecialNameChar("E", BEFORE_STYLE_4);

Tokenizer.prototype._stateBeforeStyle4 = function (c) {
	if (c === "/" || c === ">" || whitespace(c)) {
		this._special = SPECIAL_STYLE;
	}
	this._state = IN_TAG_NAME;
	this._index--;
};

Tokenizer.prototype._stateAfterStyle1 = ifElseState("Y", AFTER_STYLE_2, TEXT);
Tokenizer.prototype._stateAfterStyle2 = ifElseState("L", AFTER_STYLE_3, TEXT);
Tokenizer.prototype._stateAfterStyle3 = ifElseState("E", AFTER_STYLE_4, TEXT);

Tokenizer.prototype._stateAfterStyle4 = function (c) {
	if (c === ">" || whitespace(c)) {
		this._special = SPECIAL_NONE;
		this._state = IN_CLOSING_TAG_NAME;
		this._sectionStart = this._index - 5;
		this._index--;
	} else this._state = TEXT;
};

Tokenizer.prototype._stateBeforeEntity = ifElseState("#", BEFORE_NUMERIC_ENTITY, IN_NAMED_ENTITY);
Tokenizer.prototype._stateBeforeNumericEntity = ifElseState("X", IN_HEX_ENTITY, IN_NUMERIC_ENTITY);

Tokenizer.prototype._stateInNamedEntity = function (c) {
	if (c === ";") {
		this._parseNamedEntityStrict();
		if (this._sectionStart + 1 < this._index) {
			this._parseLegacyEntity();
		}
		this._state = this._baseState;
	} else if ((c < "a" || c > "z") && (c < "A" || c > "Z") && (c < "0" || c > "9")) {
		if (this._sectionStart + 1 === this._index);
		else if (this._baseState !== TEXT) {
			if (c !== "=") {
				this._parseNamedEntityStrict();
			}
		} else {
			this._parseLegacyEntity();
		}

		this._state = this._baseState;
		this._index--;
	}
};

Tokenizer.prototype._decodeNumericEntity = function (offset, base) {
	var sectionStart = this._sectionStart + offset;

	if (sectionStart !== this._index) {
		var entity = this._buffer.substring(sectionStart, this._index);
		var parsed = parseInt(entity, base);
		this._emitPartial(parsed);
		this._sectionStart = this._index;
	} else {
		this._sectionStart--;
	}

	this._state = this._baseState;
};

Tokenizer.prototype._stateInNumericEntity = function (c) {
	if (c === ";") {
		this._decodeNumericEntity(2, 10);
		this._sectionStart++;
	} else if (c < "0" || c > "9") {
		this._decodeNumericEntity(2, 10);
		this._index--;
	}
};

Tokenizer.prototype._stateInHexEntity = function (c) {
	if (c === ";") {
		this._decodeNumericEntity(3, 16);
		this._sectionStart++;
	} else if ((c < "a" || c > "f") && (c < "A" || c > "F") && (c < "0" || c > "9")) {
		this._decodeNumericEntity(3, 16);
		this._index--;
	}
};

Tokenizer.prototype._cleanup = function () {
	if (this._sectionStart < 0) {
		this._buffer = "";
		this._bufferOffset += this._index;
		this._index = 0;
	} else if (this._running) {
		if (this._state === TEXT) {
			if (this._sectionStart !== this._index) {
				this._cbs.ontext(this._buffer.substr(this._sectionStart));
			}
			this._buffer = "";
			this._bufferOffset += this._index;
			this._index = 0;
		} else if (this._sectionStart === this._index) {
			this._buffer = "";
			this._bufferOffset += this._index;
			this._index = 0;
		} else {
			this._buffer = this._buffer.substr(this._sectionStart);
			this._index -= this._sectionStart;
			this._bufferOffset += this._sectionStart;
		}

		this._sectionStart = 0;
	}
};

Tokenizer.prototype.write = function (chunk) {
	if (this._ended) this._cbs.onerror(Error(".write() after done!"));

	this._buffer += chunk;
	this._parse();
};

Tokenizer.prototype._parse = function () {
	while (this._index < this._buffer.length && this._running) {
		var c = this._buffer.charAt(this._index);
		if (this._state === TEXT) {
			this._stateText(c);
		} else if (this._state === BEFORE_TAG_NAME) {
			this._stateBeforeTagName(c);
		} else if (this._state === IN_TAG_NAME) {
			this._stateInTagName(c);
		} else if (this._state === BEFORE_CLOSING_TAG_NAME) {
			this._stateBeforeCloseingTagName(c);
		} else if (this._state === IN_CLOSING_TAG_NAME) {
			this._stateInCloseingTagName(c);
		} else if (this._state === AFTER_CLOSING_TAG_NAME) {
			this._stateAfterCloseingTagName(c);
		} else if (this._state === IN_SELF_CLOSING_TAG) {
			this._stateInSelfClosingTag(c);
		} else if (this._state === BEFORE_ATTRIBUTE_NAME) {
			/*
			 *	attributes
			 */
			this._stateBeforeAttributeName(c);
		} else if (this._state === IN_ATTRIBUTE_NAME) {
			this._stateInAttributeName(c);
		} else if (this._state === AFTER_ATTRIBUTE_NAME) {
			this._stateAfterAttributeName(c);
		} else if (this._state === BEFORE_ATTRIBUTE_VALUE) {
			this._stateBeforeAttributeValue(c);
		} else if (this._state === IN_ATTRIBUTE_VALUE_DQ) {
			this._stateInAttributeValueDoubleQuotes(c);
		} else if (this._state === IN_ATTRIBUTE_VALUE_SQ) {
			this._stateInAttributeValueSingleQuotes(c);
		} else if (this._state === IN_ATTRIBUTE_VALUE_NQ) {
			this._stateInAttributeValueNoQuotes(c);
		} else if (this._state === BEFORE_DECLARATION) {
			/*
			 *	declarations
			 */
			this._stateBeforeDeclaration(c);
		} else if (this._state === IN_DECLARATION) {
			this._stateInDeclaration(c);
		} else if (this._state === IN_PROCESSING_INSTRUCTION) {
			/*
			 *	processing instructions
			 */
			this._stateInProcessingInstruction(c);
		} else if (this._state === BEFORE_SPECIAL) {
			/*
			 * special tags
			 */
			this._stateBeforeSpecial(c);
		} else if (this._state === BEFORE_SPECIAL_END) {
			this._stateBeforeSpecialEnd(c);
		} else if (this._state === BEFORE_STYLE_1) {
			/*
			 * style
			 */
			this._stateBeforeStyle1(c);
		} else if (this._state === BEFORE_STYLE_2) {
			this._stateBeforeStyle2(c);
		} else if (this._state === BEFORE_STYLE_3) {
			this._stateBeforeStyle3(c);
		} else if (this._state === BEFORE_STYLE_4) {
			this._stateBeforeStyle4(c);
		} else if (this._state === AFTER_STYLE_1) {
			this._stateAfterStyle1(c);
		} else if (this._state === AFTER_STYLE_2) {
			this._stateAfterStyle2(c);
		} else if (this._state === AFTER_STYLE_3) {
			this._stateAfterStyle3(c);
		} else if (this._state === AFTER_STYLE_4) {
			this._stateAfterStyle4(c);
		} else if (this._state === BEFORE_ENTITY) {
			/*
			 * entities
			 */
			this._stateBeforeEntity(c);
		} else if (this._state === BEFORE_NUMERIC_ENTITY) {
			this._stateBeforeNumericEntity(c);
		} else if (this._state === IN_NAMED_ENTITY) {
			this._stateInNamedEntity(c);
		} else if (this._state === IN_NUMERIC_ENTITY) {
			this._stateInNumericEntity(c);
		} else if (this._state === IN_HEX_ENTITY) {
			this._stateInHexEntity(c);
		} else {
			this._cbs.onerror(Error("unknown _state"), this._state);
		}
		this._index++;
	}
	this._cleanup();
};

Tokenizer.prototype.pause = function () {
	this._running = false;
};
Tokenizer.prototype.resume = function () {
	this._running = true;

	if (this._index < this._buffer.length) {
		this._parse();
	}
	if (this._ended) {
		this._finish();
	}
};

Tokenizer.prototype.end = function (chunk) {
	if (this._ended) this._cbs.onerror(Error(".end() after done!"));
	if (chunk) this.write(chunk);
	this._ended = true;
	if (this._running) this._finish();
};

Tokenizer.prototype._finish = function () {
	if (this._sectionStart < this._index) {
		this._handleTrailingData();
	}
	this._cbs.onend();
};

Tokenizer.prototype._handleTrailingData = function () {
	var data = this._buffer.substr(this._sectionStart);
	if (this._state === IN_NAMED_ENTITY) {
		this._parseLegacyEntity();
		if (this._sectionStart < this._index) {
			this._state = this._baseState;
			this._handleTrailingData();
		}
	} else if (this._state === IN_NUMERIC_ENTITY) {
		this._decodeNumericEntity(2, 10);
		if (this._sectionStart < this._index) {
			this._state = this._baseState;
			this._handleTrailingData();
		}
	} else if (this._state === IN_HEX_ENTITY) {
		this._decodeNumericEntity(3, 16);
		if (this._sectionStart < this._index) {
			this._state = this._baseState;
			this._handleTrailingData();
		}
	} else if (
		this._state !== IN_TAG_NAME &&
		this._state !== BEFORE_ATTRIBUTE_NAME &&
		this._state !== BEFORE_ATTRIBUTE_VALUE &&
		this._state !== AFTER_ATTRIBUTE_NAME &&
		this._state !== IN_ATTRIBUTE_NAME &&
		this._state !== IN_ATTRIBUTE_VALUE_SQ &&
		this._state !== IN_ATTRIBUTE_VALUE_DQ &&
		this._state !== IN_ATTRIBUTE_VALUE_NQ &&
		this._state !== IN_CLOSING_TAG_NAME
	) {
		this._cbs.ontext(data);
	}
};

Tokenizer.prototype.reset = function () {
	Tokenizer.call(this, this._cbs);
};

Tokenizer.prototype.getAbsoluteIndex = function () {
	return this._bufferOffset + this._index;
};

Tokenizer.prototype._getSection = function () {
	return this._buffer.substring(this._sectionStart, this._index);
};

Tokenizer.prototype._emitToken = function (name) {
	this._cbs[name](this._getSection());
	this._sectionStart = -1;
};

Tokenizer.prototype._emitPartial = function (value) {
	if (this._baseState !== TEXT) {
		this._cbs.onattribdata(value);
	} else {
		this._cbs.ontext(value);
	}
};
